
package client;

import common.Client;
import common.Latency;
import common.ReceiveHandleLoop;
import common.State;
import common.TimeoutHandler;
import common.packet.Authorization;
import common.packet.ChatMessage;
import common.packet.ClientIn;
import common.packet.ClientInfo;
import common.packet.ClientOut;
import common.packet.Error;
import common.packet.GameState;
import common.packet.WorldState;
import common.packet.OK;
import common.packet.Packet;
import common.packet.PacketHandler;
import common.packet.PacketWithCode;
import common.PingHandler;
import java.awt.Canvas;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Timer;
import java.util.TimerTask;

/**
 * Provides an interface to control the game regardless of gui system and stuff.
 *
 * @author xissburg
 */
public class GameController
{
    ReceiveHandleLoop receiveHandleLoop;
    EventHandler eventHandler;
    Game game;
    Latency latency;
    Timer pingTimer;

    public GameController()
    {
        receiveHandleLoop = null;
        eventHandler = null;
        game = null;
        latency = null;
        pingTimer = null;
    }

    public void setEventHandler(EventHandler eventHandler) {
        this.eventHandler = eventHandler;
    }

    public Canvas getCanvas() {
        return game.createCanvas();
    }

    public ClientInfo getClientInfo() {
        return receiveHandleLoop.getClient().getClientInfo().copy();
    }

    public long getLatency() {
        return latency.getLatency();
    }

    public void sendChatMessage(String message)
    {
        try {
            receiveHandleLoop.getClient().write(new ChatMessage(message));
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public void logout()
    {
        if(receiveHandleLoop != null)
        {
            try {
                Client client = receiveHandleLoop.getClient();
                client.write(new PacketWithCode(Packet.Code.LOGOUT));
            } catch (IOException ex) {}
            finally {
                try {
                    receiveHandleLoop.stop();
                } catch (IOException ex) {}
            }
        }

        if(pingTimer != null)
            pingTimer.cancel();

        if(game != null)
            game.stop();

        if(eventHandler != null)
            eventHandler.disconnected(null);
    }

    public void startGame()
    {
        try {
            receiveHandleLoop.getClient().write(
                    new PacketWithCode(Packet.Code.START_GAME));
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    public void addClient(ClientInfo clientInfo)
    {
        game.addButton(clientInfo);

        if(eventHandler != null)
            eventHandler.addClient(clientInfo);
    }

    public void login(String name, String password, String hostname, int port)
    {
        //This will be set to non-null value (the actual error message
        //itself) if an error occurs
        String errorMessage = null;
        Socket socket = null;

        //Try connecting with server
        try {
            socket = new Socket(hostname, port);
        } catch (UnknownHostException ex) {
            errorMessage =  "Unknown host exception. Failed to establish connection."+
                    "\nException message: " + ex.getMessage();
        } catch (IOException ex) {
            errorMessage = "IO exception. Failed to establish connection."+
                    "\nException message: " + ex.getMessage();
        } finally {
            if(errorMessage != null)
            {
                if(eventHandler != null)
                    eventHandler.error(errorMessage);
                
                return;
            }
        }

        if(eventHandler != null)
            eventHandler.connected();

        //Create client object.
        Client client = null;
        try {
            client = new Client(socket);
        } catch (IOException ex) {
            if(eventHandler != null)
                eventHandler.error("Failed to create client object and socket.");

            //close socket
            if(client != null)
            {
                try {
                    client.closeSocket();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                }
            }
            //done here
            return;
        }

        //Set client name in its info
        client.getClientInfo().setName(name);

        //Send login info
        try {
            client.write(new Authorization(name));
        } catch (IOException ex) {
            if(eventHandler != null)
                eventHandler.error("Failed to send authentication data.");

            //close socket
            try {
                client.closeSocket();
            } catch (IOException ex1) {
                ex1.printStackTrace();
            }
            //done here
            return;
        }

        //Add OK and Error packet handlers to the receive-handle loop and start it.
        receiveHandleLoop = new ReceiveHandleLoop(client);

        receiveHandleLoop.addPacketHandler(new ErrorHandler(receiveHandleLoop));
        receiveHandleLoop.addPacketHandler(new OKHandler(receiveHandleLoop));
        receiveHandleLoop.setErrorHandler(
                    new TimeoutHandlerC(receiveHandleLoop, 5000));

        //Dispatch it to a new thread
        new Thread(receiveHandleLoop, "ReceiveHandleLoop").start();
    }

    public static interface EventHandler
    {
        void error(String message);
        void connected();
        void gameCreated();
        void addClient(ClientInfo clientInfo);
        void clientJoined(ClientInfo clientInfo);
        void clientQuit(ClientInfo clientInfo);
        void chatMessage(String message);
        void disconnected(String message);
    }

    /**
     * ErrorHandler to be used in the ReceiveHandleLoop started after sending
     * login info to the server (in the loginButtonActionPerformed method).
     * After sending login info to the server it may return an OK or an Error
     * packet. This class handles the Error. It shows the error message sent
     * from the server, reenable the main components of this panel (call to
     * setComponentsEnabled(true)) and stops the receiveHandleLoop.
     */
    private class ErrorHandler implements PacketHandler
    {
        private final ReceiveHandleLoop receiveHandleLoop;

        public ErrorHandler(ReceiveHandleLoop receiveHandleLoop) {
            this.receiveHandleLoop = receiveHandleLoop;
        }

        public void handle(Packet packet)
        {
            final Error error = (Error)packet;

            try {
                //Close socket and stop the RHL. Its run() method will now return
                //which means that if it is the runnable of a thread, the thread
                //will die.
                receiveHandleLoop.stop();
            } catch (IOException ex1) {
                ex1.printStackTrace();
            }

            if(eventHandler != null)
                eventHandler.disconnected(error.getMessage());
        }

        public Packet.Code getPacketCode() {
            return Packet.Code.ERROR;
        }
    }


    /**
     * Handles the OK packet. After receiving an OK, the client must add a
     * GameState handler to its RHL and also set its default handler to a
     * packet handler which enqueues all packets because before receiving the
     * GameState it is unsafe to process any other packets (for example, a set
     * position coming from some client. It will cause an error if the client
     * tries to process it because it still doesn't have the client list, which
     * is sent in the GameState packet). The GameState handler sets up the next
     * screen and process all packets in the queue if any.
     */
    private class OKHandler implements PacketHandler
    {
        private final ReceiveHandleLoop receiveHandleLoop;

        public OKHandler(ReceiveHandleLoop receiveHandleLoop) {
            this.receiveHandleLoop = receiveHandleLoop;
        }

        public void handle(Packet packet)
        {
            //remove this OK handler
            receiveHandleLoop.removePacketHandler(getPacketCode());

            //Add GameState packet handler, and add the Enqueue handler
            //as default so that all other messages will be enqueued and
            //later processed when the GameState packet arrives.
            EnqueueDefaultHandler edh = new EnqueueDefaultHandler();
            receiveHandleLoop.setDefaultHandler(edh);
            receiveHandleLoop.addPacketHandler(
                    new GameStateHandler(receiveHandleLoop, edh));

            //Send OK to notify the server that the client is ready to continue
            //the process
            Client client = receiveHandleLoop.getClient();
            try {
                client.write(new OK());
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }

        public Packet.Code getPacketCode() {
            return Packet.Code.OK;
        }
    }


    /**
     * Handles the awaited GameState packet. First it removes itself from the
     * RHL and sets the default handler to null. Then it builds the new screen
     * using the GameState packet data to add stuff to it. Then it adds handlers
     * for ClientIn/Out, ChatMessage and SetPosition packets in the same RHL.
     * Then it process all packets in the EnqueueDefaultHandler's queue(by calling
     * the RHL's handle method for each packet).
     */
    private class GameStateHandler implements PacketHandler
    {
        private final ReceiveHandleLoop receiveHandleLoop;
        private final EnqueueDefaultHandler enqueueDefaultHandler;

        public GameStateHandler(ReceiveHandleLoop receiveHandleLoop,
                                EnqueueDefaultHandler enqueueDefaultHandler) {
            this.receiveHandleLoop = receiveHandleLoop;
            this.enqueueDefaultHandler = enqueueDefaultHandler;
        }

        public void handle(Packet packet)
        {
            final GameState gameState = (GameState)packet;

            //Create the game object
            game = new Game(receiveHandleLoop.getClient());

            latency = new Latency(receiveHandleLoop.getClient());

            //Setup the new packet handlers set
            receiveHandleLoop.removePacketHandler(getPacketCode());
            receiveHandleLoop.addPacketHandler(new PingHandler(receiveHandleLoop.getClient()));
            receiveHandleLoop.addPacketHandler(latency.getPongHandler());
            receiveHandleLoop.addPacketHandler(new ClientInHandler());
            receiveHandleLoop.addPacketHandler(new ClientOutHandler());
            receiveHandleLoop.addPacketHandler(new ChatMessageHandler());
            receiveHandleLoop.addPacketHandler(new TimeoutHandler.
                    AreYouAliveHandler(receiveHandleLoop.getClient()));
            receiveHandleLoop.addPacketHandler(new ClientInfoHandler());
            receiveHandleLoop.addPacketHandler(new StartGameHandler());
            receiveHandleLoop.addPacketHandler(new WorldStateHandler());
            
            receiveHandleLoop.setDefaultHandler(null);
            
            //Set a TimeoutHandler to identify server inactivity which usually means
            //it is down. Set a higher timeout than that of the server so that there
            //won't ever be timeouts in the client (because of the server's timeout
            //handler), except when the server actually stops responding because it
            //is down.
            int maxInactivityTime = 15000;
            try {
                receiveHandleLoop.getClient().setSocketTimeout(maxInactivityTime);
            } catch (SocketException ex) {
                ex.printStackTrace();
                try {
                    receiveHandleLoop.stop();
                } catch (IOException ex1) {
                    ex1.printStackTrace();
                }
                return;
            }

            //Find the ClientInfo of this client in the client collection in the
            //GameState packet and update the clientInfo contained in the client
            //object in the RHL. Do it before calling gameCreated() because the
            //callee may need this updated data.
            final Collection<ClientInfo> clients = gameState.getClients();
            final ClientInfo thisCi = receiveHandleLoop.getClient().getClientInfo();
            boolean found = false;

            Iterator<ClientInfo> it = clients.iterator();

            while(it.hasNext())
            {
                ClientInfo ci = it.next();

                if(thisCi.equals(ci))
                {
                    thisCi.set(ci);
                    //remove ci and add thisCi to the clients collection so that
                    //when adding all these clients later on, this instance will
                    //be added instead of the one which came from the server.
                    it.remove();
                    clients.add(thisCi);
                    found = true;
                    break;
                }
            }

            if(!found)
            {
                //Something really crazy happened. Stop this now
                if(eventHandler != null)
                    eventHandler.error("You are not in the servers' client list.");
                try {
                    receiveHandleLoop.stop();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                return;
            }

            //Notify that the game is ready to run
            if(eventHandler != null)
                eventHandler.gameCreated();

            //Add the clients
            for(ClientInfo ci: clients)
            {
                addClient(ci);
            }
            
            //handle all packets in the queue. Here we are in the RHL thread
            Queue<Packet> packetQueue = enqueueDefaultHandler.getPacketQueue();
            for(Packet p: packetQueue)
                receiveHandleLoop.handle(p);

            if(gameState.getState() == State.RUNNING)
            {
                receiveHandleLoop.removePacketHandler(Packet.Code.CLIENT_INFO);
                receiveHandleLoop.removePacketHandler(Packet.Code.START_GAME);
                game.start();
            }
            else if(gameState.getState() == State.WAITING)
            {
                receiveHandleLoop.removePacketHandler(Packet.Code.WORLD_STATE);
            }

            pingTimer = new Timer();
            pingTimer.schedule(new TimerTask(){
                @Override
                public void run() {
                    latency.refreshLatency();
                }

            }, 2000, 4000);
        }

        public Packet.Code getPacketCode() {
            return Packet.Code.GAME_STATE;
        }
    }


    /**
     * Enqueues packets received in the handle method.
     */
    private static class EnqueueDefaultHandler implements PacketHandler
    {
        private Queue<Packet> packetQueue;

        public EnqueueDefaultHandler()
        {
            packetQueue = new LinkedList<Packet>();
        }

        public void handle(Packet packet)
        {
            packetQueue.offer(packet);
        }

        public Packet.Code getPacketCode() {
            return null;
        }

        public Queue<Packet> getPacketQueue() {
            return packetQueue;
        }
    }

    private class ClientInHandler implements PacketHandler
    {
        public void handle(Packet packet) {
            ClientIn clientIn = (ClientIn)packet;

            game.addButton(clientIn.getClientInfo());

            if(eventHandler != null)
                eventHandler.clientJoined(clientIn.getClientInfo());
        }

        public Packet.Code getPacketCode() {
            return Packet.Code.CLIENT_IN;
        }
    }

    private class ClientOutHandler implements PacketHandler
    {
        public void handle(Packet packet) {
            ClientOut clientOut = (ClientOut)packet;
            String name = clientOut.getName();
            ClientInfo clientInfo = game.getClientInfo(name);
            game.removeButton(name);

            if(eventHandler != null)
                eventHandler.clientQuit(clientInfo);
        }

        public Packet.Code getPacketCode() {
            return Packet.Code.CLIENT_OUT;
        }
    }

    private class ChatMessageHandler implements PacketHandler
    {
        public void handle(Packet packet)
        {
            ChatMessage chatMessage = (ChatMessage)packet;

            if(eventHandler != null)
                eventHandler.chatMessage(chatMessage.getMessage());
        }

        public Packet.Code getPacketCode() {
            return Packet.Code.CHAT_MESSAGE;
        }
    }

    private class ClientInfoHandler implements PacketHandler
    {
        public void handle(Packet packet)
        {
            ClientInfo clientInfo = (ClientInfo)packet;
            game.setClientInfo(clientInfo);
        }

        public Packet.Code getPacketCode() {
            return Packet.Code.CLIENT_INFO;
        }
    }

    private class StartGameHandler implements PacketHandler
    {
        public void handle(Packet packet)
        {
            receiveHandleLoop.removePacketHandler(getPacketCode());
            receiveHandleLoop.removePacketHandler(Packet.Code.CLIENT_INFO);
            receiveHandleLoop.addPacketHandler(new WorldStateHandler());
            game.start();
        }

        public Packet.Code getPacketCode() {
            return Packet.Code.START_GAME;
        }
    }

    private class WorldStateHandler implements PacketHandler
    {
        public void handle(Packet packet)
        {
            WorldState worldState = (WorldState)packet;
            worldState.setLatency(getLatency());
            game.setWorldState(worldState);
        }

        public Packet.Code getPacketCode() {
            return Packet.Code.WORLD_STATE;
        }
    }

    private class TimeoutHandlerC extends TimeoutHandler
    {
        public TimeoutHandlerC(ReceiveHandleLoop receiveHandleLoop, int timeout)
        {
            super(receiveHandleLoop, timeout);
        }
        
        @Override
        public void timedout()
        {
            game.stop();

            if(eventHandler != null)
                eventHandler.disconnected("Server is not responding.");
        }

        @Override
        public boolean onIOException(IOException ex)
        {
            if(game != null)
                game.stop();

            if(eventHandler != null)
                eventHandler.disconnected("I/O exception: " + ex.getMessage());

            return false;
        }
    }
}
